package calao.client.util;

import java.util.HashMap;

/** A range of dates (calendar days).
 */
public class DateRange 
{
	
	public class IndexRange 
	{
		int start = 0;
		int end = dayCount-1;
		private IndexRange() { }
		public int getStart() {
			return start;
		}
		public int getEnd() {
			return end;
		}
		public int length() {
			return end-start+1;
		}
	}

	static class Day extends CalendarDate
	{
		int index = -1;
		int nextIndex = -1;
		int prevIndex = -1;	
		boolean firstOfWeek = false;
		Day(CalendarDate date) {
			super(date.year, date.month, date.day);
		}
		Day(Day day) {
			super(day);
		}
		Day nextDay() {
			Day day = new Day(this);
			day.incDay(1);
			return day;
		}
	}
	
	static class DayMap extends HashMap 
	{
		Day getDay(CalendarDate date)
		{
			boolean timeSignificant = date.isTimeSignificant();
			date.setTimeSignificant(false);
			try {
				return (Day)get(date);
			}
			finally {
				date.setTimeSignificant(timeSignificant);
			}
		}
	}
	
	DayMap dayMap = new DayMap();
	Day[] days;

	final Day startDay;
	final Day endDay;

	int removedWeekdays = 0;
	int dayCount;

	public DateRange(long startDate, long endDate)
	{
		this(new CalendarDate(startDate), new CalendarDate(endDate));
	}
		
	public DateRange(CalendarDate startDate, CalendarDate endDate)
	{
		this.startDay = new Day(startDate);
		this.endDay = new Day(endDate);
	}

	protected void computeDays()
	{
		int dayIndex = -1;
		int nextIndex = 0;
		int maxDays = endDay.countDaysFrom(startDay)+2;
		days = new Day[maxDays];
		boolean firstOfWeek = false;
		for (Day day = startDay; day.compareDateTo(endDay)<=0; day = day.nextDay()) {
			day.prevIndex = dayIndex;
			if (day.isFirstDayOfWeek()) firstOfWeek = true;
			if (!hasWeekDay(day.getWeekDay())) {
				dayIndex = nextIndex;
				nextIndex++;
				day.index = dayIndex;
				days[dayIndex] = day;
				if (firstOfWeek) {
					day.firstOfWeek = firstOfWeek;
					firstOfWeek = false;
				}
			}
			day.nextIndex = nextIndex;
			dayMap.put(day, day);
		}
		dayCount = nextIndex;
	}
	
	public void removeWeekDay(int day)
	{
		removedWeekdays |= (1<<day);
	}
	
	boolean hasWeekDay(int day)
	{
		return ((1<<day)&removedWeekdays)!=0;
	}
	
	public CalendarDate getDay(int index)
	{
		if (days==null) computeDays();
		return days[index].copy();
	}
	
	/** Tells whether a day is the first day of its week in this range.
	 */
	public boolean isFirstDayOfWeek(int index)
	{
		if (days==null) computeDays();
		return days[index].firstOfWeek;
	}

	public CalendarDate getFirstDay()
	{
		return dayCount>0? getDay(0) : null;
	}

	public CalendarDate getLastDay()
	{
		return dayCount>0? getDay(dayCount-1) : null;
	}

	public int getDayCount()
	{
		if (days==null) computeDays();
		return dayCount;
	}

	public CalendarDate getEndDate() 
	{
		return endDay.copy();
	}

	public CalendarDate getStartDate() 
	{
		return startDay.copy();
	}
	
	public long getStartTime()
	{
		return startDay.getTimeInMillis();
	}
	
	public long getEndTime()
	{
		return endDay.getTimeInMillis()+CalendarDate.DAY_MILLIS-1;
	}

	public IndexRange getPeriodDays(long periodStart, long periodEnd)
	{
		return getPeriodDays(new CalendarDate(periodStart), new CalendarDate(periodEnd));
	}
	
	/** Gets days in this range that match a period of time.
	 * @return A valid index range if the period has days in this range, null otherwise.
	 */
	public IndexRange getPeriodDays(CalendarDate periodStart, CalendarDate periodEnd)
	{
		if (days==null) computeDays();
		IndexRange range = new IndexRange();
		Day pstartDay = dayMap.getDay(periodStart);
		Day pendDay = dayMap.getDay(periodEnd);
		if (pstartDay!=null) {
			range.start = pstartDay.index!=-1? pstartDay.index : pstartDay.nextIndex;
			if (range.start==dayCount) return null;
		}
		if (pendDay!=null) {
			range.end = pendDay.index!=-1? pendDay.index : pendDay.prevIndex;
		}
		if (pstartDay==null && pendDay==null) {
			if (periodEnd.compareDateTo(startDay)<0) return null;
			if (periodStart.compareDateTo(endDay)>0) return null;
		}
		return range;
	}
	
	/** Gets days in this range that match a month.
	 * @return A valid index range if the month has days in this range, null otherwise.
	 */
	public IndexRange getMonthDays(CalendarDate month)
	{
		CalendarDate start = new CalendarDate(month.getYear(), month.getMonth(), 1);
		CalendarDate end = start.copyWithOffset(0, 1, -1);
		return getPeriodDays(start, end);
	}
	
	public String toString()
	{
		return "{"+startDay+", "+endDay+"}";
	}

}
